/*

Six-core aviation socket:
1 temp sensor +
2 brown: air +
3 temp sensor -
4 white: froth -
5 blue: froth +
6 black: air - 
*/

#include <Arduino.h>
#include <Wire.h>
#include <LCD.h>
#include <LiquidCrystal_I2C.h>
#include <EEPROM.h>
#include <max6675.h>

#define VERSION "0302-05"
#define HEATING_TARGET 56   // 60
// #define INSULATION_TEMP 20  // Start Insulation temperature

// LiquidCrystal display
// SDA --> A4
// SCL --> A5
LiquidCrystal_I2C lcd(0x3F, 2, 1, 0, 4, 5, 6, 7);
// Joystick pin:
#define pinX A0
#define pinY A1
#define pinKey 2     // digital pin 2
#define pinBuzzer 3  // digital pin 3

#define pinAir 9  // digital pin 9 PWM
#define pinFroth 10 // digital pin 10 PWM
#define pinHeat  4  // digital pin 4

// K Thermocouple 
const float TEMP_COMP = 4.5;
int ktcCLK = 6;
int ktcCS = 7;
int ktcSO = 8;

MAX6675 ktc(ktcCLK, ktcCS, ktcSO);

int valueX = 0;
int valueY = 0;
uint16_t keyValue = 0;
uint8_t buttonValue = HIGH;
int address = 30; // EEPROM start address

/*
Input: 12V/3.3A
Power(%)  PWM Output(V)
18        3.2   Air power
38        5.7
40        6.4
27        3.6
0         0.00
*/
// Milk heat(0:No 1:Yes), Air time(s), Froth time(s), Air power(%), Dense time(s),Froth power(%),Dense power(%)
uint16_t valueProf[] = {1, 15, 60, 15, 18, 39, 30, 18};  
const uint8_t stepProf[] = {1, 1, 1, 1, 1, 1, 1, 1};  // value change step
const uint8_t motorType[] = {pinAir, pinAir, pinFroth, pinFroth, pinFroth};
const char *titleProf[] = {"Milk heating:   ", "Air time(s):    ", "Froth time(s):  ",
                           "Dense foam(s)   ", "Air power(%)    ", "Froth power(%)  ", 
                           "Dense power(%)  ", "Stir Power(%)   "};
const uint16_t minProf[] = {0, 0, 0, 0, 0, 0, 0, 0}; // value change minimum
const uint16_t maxProf[] = {1, 200, 600, 600, 100, 100, 100, 100};  // value change maximum

// globel variable
uint8_t gFoamSize = 4;  // for phase control
uint8_t gFormArrayStart = 0;  // show start index for titleProf,0 start with Milk heating, 1 for Air times.
uint8_t gArraySize = 8;  // total array size
uint8_t gIndexY = gFormArrayStart + 1;
uint8_t gRestart = 0;
unsigned long gLastTimer = 0; // Timer count in main loop
uint16_t gSecLoopCount = 0;        // every second loop count
uint8_t gPhase = 0;  // phase second from valueProf array.
int gCountdown = -1;  // count down seconds till end of phase
int gCalcTime = 0;
int out = 0;  // write to PWM pin
int gHeated = 0; // 0: Target temperature not reached. 1: readched
float gCurTemp = 0; 
uint8_t gDoHeating = 0;
int gTotalCount = 0;
// Milk heating control
const uint8_t STIR_WAIT_TIME = 10;  // second
const uint8_t STIR_TIME = 5;  // second
uint8_t curStirWait = 0;
uint8_t curStirTime = 0;
bool stirFlag = false;  // false: wait time, true: stir milk

// function declare
void adjValue(uint8_t idx);
void setParam(uint8_t idx);
uint8_t keyScanY(void);
void lcdDisplay();
void doTimerLoop();
void storeArray();
void loadArray();
void EEPROMWriteInt(int address, int value);
int EEPROMReadInt(int address);
void beep(int t, int num);
void doHeating();
void tempDisplay();
void heatDisplay();
void tempDisplay(uint8_t x, uint8_t y);
void waitKeyUp();
void stirMilk();
void frothVolDisplay(uint8_t x, uint8_t y);
void doPreheat();

void doEverySecond()
{
  gCurTemp = ktc.readCelsius();
  gCurTemp -= TEMP_COMP;

  if (gCountdown <= 0 && gPhase < gFoamSize)
  {
    analogWrite(motorType[gPhase], 0);  // stop
    gPhase++;
    if (gPhase < gFoamSize) {
      gCalcTime = 0;
      uint16_t phaseTime = valueProf[gPhase];
      if (phaseTime > 0)
      {
        gCalcTime = millis() / 1000;
        gCalcTime = gCalcTime + phaseTime;
        // control motor by phase array
        uint8_t index = gFoamSize + gPhase - 1;
        out = map(valueProf[index], 0, 100, 0, 255);
        Serial.print("Power(%): ");
        Serial.print(valueProf[index]);
        Serial.print(" Control motor: ");
        Serial.print(motorType[gPhase]);
        Serial.print(" PWM: ");
        Serial.println(out);
        analogWrite(motorType[gPhase], out);
      }
    } else {
      // all done
      Serial.print("Phase end: ");
      Serial.println(gPhase);
      lcd.clear();
      lcd.home();
      lcd.print("   Completed!   ");
      gRestart = 1;
      beep(100, 3);
    }
  
  }
  if (gPhase < gFoamSize)
  {
    gCountdown = gCalcTime - millis() / 1000;
    if (gCountdown > 0) {
      gTotalCount--;
      lcdDisplay();
    }
  }
}



void doTimerLoop()
{
  // do every second
  if (gSecLoopCount > 1)
  {
    gSecLoopCount = 0;
    doEverySecond();
  }
  gSecLoopCount++;
}

void lcdDisplay() {
  // write to LCD screen
  lcd.clear();
  lcd.home();
  lcd.print(titleProf[gPhase]);
  lcd.setCursor(0, 1);
  lcd.print(gCountdown);
  lcd.setCursor(3, 1);
  lcd.print("Tot:");
  lcd.print(gTotalCount);
  tempDisplay(10, 1);
  
}

void setup()
{
  digitalWrite(pinHeat, HIGH); // OFF
  pinMode(pinX, INPUT);
  pinMode(pinY, INPUT);
  pinMode(pinKey, INPUT_PULLUP);
  pinMode(pinBuzzer, OUTPUT);
  pinMode(pinHeat, OUTPUT);

  digitalWrite(pinBuzzer, HIGH); // HIGH, stop beep
  pinMode(pinAir, OUTPUT);   // sets the digital pin as output
  pinMode(pinFroth, OUTPUT); // sets the digital pin as output
  analogWrite(pinAir, 0);
  analogWrite(pinFroth, 0);

  Serial.begin(9600);

  lcd.begin(16, 2);
  lcd.setBacklightPin(3, POSITIVE); // POSITIVE,NEGATIVE
  lcd.setBacklight(HIGH);           // clear LCD screen
  lcd.clear();
  lcd.home();
  lcd.print("   Auto Former   ");
  lcd.setCursor(0, 1);
  lcd.print("Ver 1.0 ");
  lcd.print(VERSION);
  delay(1000);
  lcd.clear();  
}

void mySetup()
{

  gRestart = 0;
  gIndexY = gFormArrayStart + 1;
  gLastTimer = 0;
  gSecLoopCount = 0;   
  gPhase = 0;           
  gCountdown = -1;           // count down seconds till end of phase
  gCalcTime = 0;
  gHeated = 0;
  gDoHeating = 0;
  gTotalCount = 0;
  curStirTime = 0;
  curStirWait = 0;
  stirFlag = false;

  digitalWrite(pinHeat, HIGH);  // OFF

  analogWrite(pinAir, 0);
  analogWrite(pinFroth, 0);
  lcd.clear();
  lcd.home();
  loadArray();
  setParam(gFormArrayStart);
  delay(600);
  waitKeyUp();
  Serial.print("buttonValue: ");
  Serial.println(buttonValue);
  delay(10);
  while (buttonValue == HIGH)
  {
    // show temperature
    if (millis() - gLastTimer >= 1000)
    {
      gLastTimer = millis();
      gCurTemp = ktc.readCelsius();
      gCurTemp -= TEMP_COMP;
      tempDisplay(10, 1);
    }

    keyValue = keyScanY();

    if (keyValue != 0)
    {
      Serial.println(gIndexY);
      setParam(gIndexY - 1);
    }
    adjValue(gIndexY - 1);
    buttonValue = digitalRead(pinKey);
  }
  gLastTimer = 0;
  storeArray();
  // total run time(seconds)
  for (int i = 1; i < gFoamSize; i++) {
    gTotalCount += valueProf[i];
  }    
  // gTotalCount++;
  beep(200, 1);
  waitKeyUp();
  Serial.print("Total run time: ");
  Serial.println(gTotalCount);
  Serial.println("setup run over!");
  lcd.clear();
  lcd.home();
}

void tempDisplay() {
  lcd.setCursor(0, 1);
  lcd.print("TEMP:");
  lcd.print(gCurTemp, 1);
  lcd.print((char)223);
  lcd.print("C  ");
}

void tempDisplay(uint8_t x, uint8_t y)
{
  lcd.setCursor(x, y);
  //lcd.print("T:");
  lcd.print(gCurTemp, 1);
  lcd.print((char)223);
  lcd.print("C  ");
}


void heatDisplay() {
  lcd.home();
  lcd.print("Heating Milk... ");
  tempDisplay();
}

void lcdValueDisplay(uint8_t idx)
{
  uint16_t val = valueProf[idx];
  lcd.setCursor(0, 1);
  if (idx == 0)
  {
    if (val == 0)
    {
      lcd.print("No");
    } else {
      lcd.print("Yes");
    }
  } else 
  lcd.print(val);
  lcd.print("    ");
}

void doHeating() {
  gCurTemp = ktc.readCelsius();
  gCurTemp -= TEMP_COMP;
  if (gCurTemp > 102) {  // read temp error
    return;
  }
  if (gDoHeating == 0) {
    digitalWrite(pinHeat, LOW);  // ON
    gDoHeating = 1;
  }
  heatDisplay();
  stirMilk();
  if (gCurTemp >= HEATING_TARGET)
  {
    // heating finished
    digitalWrite(pinHeat, HIGH);  // OFF
    gHeated = 1;
    analogWrite(pinFroth, 0);  // stop stir
    beep(100, 2);
  }
}

void stirMilk() {
  if (stirFlag)
  {
    curStirTime++;
    if (curStirTime > STIR_TIME)
    {
      stirFlag = false;
      curStirTime = 0;
      analogWrite(pinFroth, 0);  // stop stir
    }
    Serial.print("Stir milk: ");
    Serial.println(curStirTime);
  }
  else
  {
    curStirWait++;
    if (curStirWait > STIR_WAIT_TIME)
    {
      stirFlag = true;
      curStirWait = 0;
      int stirPower = map(valueProf[7], 0, 100, 0, 255);
      Serial.print("Stir power: ");
      Serial.println(stirPower);
      analogWrite(pinFroth, stirPower); // start stir
    }
    Serial.print("Wait time: ");
    Serial.println(curStirWait);
  }
}

void waitKeyUp() {
  while (buttonValue == LOW) {
    delay(10);
    buttonValue = digitalRead(pinKey);
  }
}

void doPreheat() {
  lcd.setCursor(0, 0);
  lcd.print("Preheat ready");
  lcd.setCursor(0, 1);
  lcd.print("Press to start");
  buttonValue = digitalRead(pinKey);
  while (buttonValue == HIGH) {
    delay(20);
    buttonValue = digitalRead(pinKey);
  }
  waitKeyUp();
  digitalWrite(pinHeat, LOW); // ON
  lcd.clear();
  lcd.home();
  lcd.setCursor(0, 0);
  lcd.print("Preheating...   ");
  lcd.setCursor(0, 1);
  lcd.print("Press to stop   ");
  buttonValue = digitalRead(pinKey);
  while (buttonValue == HIGH)
  {
    delay(20);
    buttonValue = digitalRead(pinKey);
  }
  waitKeyUp();
  digitalWrite(pinHeat, HIGH); // OFF
  lcd.clear();
  lcd.home();
}

void loop()
{
  mySetup();
  waitKeyUp();
  if (valueProf[0]) {
    doPreheat();
  }
  while (valueProf[0] && !gHeated && buttonValue == HIGH)
  {
    if (millis() - gLastTimer >= 1000)
    {
      gLastTimer = millis();
      doHeating();
    }
    buttonValue = digitalRead(pinKey);
  }
  waitKeyUp();
  digitalWrite(pinHeat, HIGH); // OFF
  analogWrite(pinFroth, 0);
  analogWrite(pinAir, 0);
  while (gRestart == 0 && buttonValue == HIGH)
  {
    if (millis() - gLastTimer >= 500)
    {
      gLastTimer = millis();
      doTimerLoop();
    }
    buttonValue = digitalRead(pinKey);
    delay(20);
  }  // end while
}

// *******************************************************
void setParam(uint8_t idx)
{
  lcd.setCursor(0, 0);
  lcd.println(titleProf[idx]);
  uint16_t val = valueProf[idx];
  Serial.println(val);
  lcdValueDisplay(idx);
}

void adjValue(uint8_t idx)
{
  uint8_t step = stepProf[idx];
  uint16_t min = minProf[idx];
  uint16_t max = maxProf[idx];
  static uint8_t keyUp = 1;
  valueX = analogRead(pinX);

  if (keyUp && valueX <= 10)
  {
    delay(10);
    keyUp = 0;
    if (valueProf[idx] < max)
    {
      valueProf[idx] = valueProf[idx] + step;
      lcdValueDisplay(idx);
    }
  }
  else if (keyUp && valueX >= 1010)
  {
    delay(10);
    keyUp = 0;
    if (valueProf[idx] > min)
    {
      valueProf[idx] = valueProf[idx] - step;
      lcdValueDisplay(idx);
    }
  }
  else if ((valueX > 10) && (valueX < 1010))
    keyUp = 1;
}

uint8_t keyScanY(void)
{
  static unsigned char keyUp = 1;
  valueY = analogRead(pinY);

  if (keyUp && (valueY <= 10))
  {
    delay(10);
    keyUp = 0;
    if (gIndexY > (gFormArrayStart + 1))
    {
      --gIndexY;
      return gIndexY;
    }
  }
  else if (keyUp && (valueY >= 1010))
  {
    delay(10);
    keyUp = 0;
    if (gIndexY < gArraySize)
    {
      ++gIndexY;
      return gIndexY;
    }
  }
  else if ((valueY > 10) && (valueY < 1010))
    keyUp = 1;
  return 0;  // not change
}

uint8_t keyScanYBak(void)
{
  static unsigned char keyUp = 1;
  valueY = analogRead(pinY);

  if (keyUp && ((valueY <= 10) || (valueY >= 1010)))
  {
    delay(10);
    keyUp = 0;
    if (valueY <= 10)
    {
      if (gIndexY > (gFormArrayStart + 1))
      {
        --gIndexY;
        return gIndexY;
      }
    }
    else if (valueY >= 1010)
    {
      delay(10);
      keyUp = 0;
      if (gIndexY < gArraySize)
      {
        ++gIndexY;
        return gIndexY;
      }
    }
  }
  else if ((valueY > 10) && (valueY < 1010))
    keyUp = 1;
  return 0;
}

void storeArray()
{
  int i;
  for (i = 0; i < gArraySize; i++)
  {
    EEPROMWriteInt(address + i * 2, valueProf[i]);
  }
}

void loadArray() {
  uint8_t k = EEPROM.read(address);
  // first time run
  if (k == 255 ) {
    return;
  }
  int i;
  for (i = 0; i < gArraySize; i++)
  {
    valueProf[i] = EEPROMReadInt(address + i * 2);
  }
}

void EEPROMWriteInt(int address, int value)
{
  byte two = (value & 0xFF);
  byte one = ((value >> 8) & 0xFF);

  EEPROM.update(address, two);
  EEPROM.update(address + 1, one);
}

int EEPROMReadInt(int address)
{
  long two = EEPROM.read(address);
  long one = EEPROM.read(address + 1);

  return ((two << 0) & 0xFFFFFF) + ((one << 8) & 0xFFFFFFFF);
}

/**
 * @param t : beep how long (ms) 
 *  @param num : beep times
 */
void beep(int t, int num) {
  for (int i = 0; i < num; i++) {
    digitalWrite(pinBuzzer, LOW); // LOW, beep
    delay(t);                     // wait 1000ms
    digitalWrite(pinBuzzer, HIGH); // HIGH, stop beep
    delay(200);
  }
}